/* 
内存:
计算机的内存是一块用于存储数据的空间，由一系列连续的存储单元组成
    bit : 0, 1
    每一个单元格都表示 1 个 Bit，一个 bit 在 EE 专业的同学看来就是高低电位，而在 CS 同学看来就是 0、1 两种状态。
    8bit = 1byte
内存编址：
    将 byte 作为内存寻址的最小单元，也就是给每个 byte 一个编号，这个编号就叫内存的地址

    现在的计算机一般都是 32 位起步了，32 位意味着可寻址的内存范围是 2^32 byte = 4GB
    2**32/1024/1024/1024 = 4.0   GB/MB/KB/Byte

    如果你的电脑是 32 位的，那么你装超过 4G 的内存条也是无法充分利用起来的
*/
// 源码 整数相加 --> 正负相加 不等于 0
// 反码 正负相加 --> 负数相加 不对 
    // 最大的优点，就在于解决了减法的问题
    // 反码表示法不仅不会像原码那样出现两个相反数相加不为零的情况，
    // 而且对于任意的一个正数加负数的计算结果是正确的
// 补码 负数相加 --> 
    // 二进制数在内存中以补码的形式存储
    // 补码即解决了符号问题，也解决了负数加负数的情况
// 移码 --表示浮点数的阶码
/*
变量的本质
    变量定义的时候，实际上是向内存申请了一块空间来存放你的变量
    int 类型占 4 个字节，并且在计算机中数字都是用补码

    999 换算成补码就是：0000 0011 1110 0111
    这里有 4 个byte，所以需要四个单元格来存储：高位的字节放在了低地址的地方

    将高位字节放在内存低地址的方式叫做大端
    将低位字节放在内存低地址的方式就叫做小端

*/

/*
指针是什么东西？

    变量放在哪？
        运算符&来取得变量实际的地址，这个值就是变量所占内存块的起始地址
        实际上这个地址是虚拟地址，并不是真正物理内存上的地址
        可以把这个地址打印出来: printf("%x", &a); --> 一串数字:0x7ffcad3b8f3c
指针本质
    通过&符号获取变量的内存地址，那获取之后如何来表示这是一个地址，而不是一个普通的值呢？

    在 C 语言中如何表示地址这个概念呢？ 指针
    int *pa = &a;
    pa 中存储的就是变量 a 的地址，也叫做指向 a 的指针。

    为什么我们需要指针？直接用变量名不行吗？
    当然可以，但是变量名是有局限的。

    变量名的本质是什么？
    是变量地址的符号化，变量是为了让我们编程时更加方便，对人友好，可计算机可不认识什么变量 a，它只知道地址和指令。

    很多语言为了安全性，给指针戴上了一层枷锁，将指针包装成了引用。

解引用
    pa中存储的是a变量的内存地址，那如何通过地址去获取a的值呢？
    运算符 *就可以拿到一个指针所指地址的内容

    指针存储的是变量内存的首地址，那编译器怎么知道该从首地址开始取多少个字节呢？
    编译器会根据指针的所指元素的类型去判断应该取多少个字节
    pa 指针首先是一个变量，它本身也占据一块内存，这块内存里存放的就是 a 变量的首地址
    当解引用的时候，就会从这个首地址连续划出 4 个 byte，然后按照 int 类型的编码方式解释。


*/
struct fraction {
	int num; // 整数部分
	int denom; // 小数部分
};

struct fraction fp;
fp.num = 10;
fp.denom = 2;
/*
结构体和指针
    结构体的本质其实就是一堆的变量打包放在一起，而访问结构体中的域，就是通过结构体的起始地址，也叫基地址，然后加上域的偏移。
    把 10 放在了结构体中基地址(fp)偏移为 0 的域，2 放在了偏移为 4 的域

    C 语言是基础，你真正懂了 C 指针和内存，对于其它语言你也会很快的理解其对象模型以及内存布局。
*/
int a;
int *pa = &a;
int **ppa = &pa;
int ***pppa = &ppa;
/*
多级指针
    没有多级指针这种东西，指针就是指针，多级指针只是为了我们方便表达而取的逻辑概念。

    格子就是内存单元，编号就是地址，格子里放的东西就对应存储在内存中的内容
    假设我把一本书，放在了 03 号格子，然后把 03 这个编号告诉你，你就可以根据 03 去取到里面的书。
    那如果我把书放在 05 号格子，然后在 03 号格子只放一个小纸条，上面写着：「书放在 05 号」。

    你会怎么做？

    当然是打开 03 号格子，然后取出了纸条，根据上面内容去打开 05 号格子得到书。
    这里的 03 号格子就叫指针，因为它里面放的是指向其它格子的小纸条（地址）而不是具体的书。

    那我如果把书放在 07 号格子，然后在 05 号格子 放一个纸条：「书放在 07号」，同时在03号格子放一个纸条「书放在 05号」
    这里的 03 号格子就叫二级指针，05 号格子就叫指针，而 07 号就是我们平常用的变量。

    依次，可类推出 N 级指针。

    所以你明白了吗？同样的一块内存，如果存放的是别的变量的地址，那么就叫指针，存放的是实际内容，就叫变量。

    pa 就叫一级指针，也就是平时常说的指针，ppa 就是二级指针

不管几级指针有两个最核心的东西：
    指针本身也是一个变量，需要内存去存储，指针也有自己的地址
    指针内存存储的是它所指向变量的地址
这就是我为什么说多级指针是逻辑上的概念，实际上一块内存要么放实际内容，要么放其它变量地址，就这么简单

怎么去解读int **a这种表达呢？
    int ** a 可以把它分为两部分看，即 int* 和 *a，后面 *a 中的*表示 a 是一个指针变量，前面的 int* 表示指针变量 a
    只能存放 int* 型变量的地址。

对于二级指针甚至多级指针，我们都可以把它拆成两部分。
    首先不管是多少级的指针变量，它首先是一个指针变量，
        指针变量就是一个*，
        其余的*表示的是这个指针变量只能存放什么类型变量的地址。
    比如int****a表示指针变量 a 只能存放 int*** 型变量的地址。

*/
int array[10] = {10, 9, 8, 7};
printf("%d\n", *array);  // 	输出 10
printf("%d\n", array[0]);  // 输出 10

int *pa = array;
printf("%d\n", *pa);  // 	输出 10
printf("%d\n", pa[0]);  // 输出 10

printf("%u", sizeof(array)); // 输出 40
printf("%u", sizeof(pa)); // 输出 4
//第一个将会输出 40，因为 array包含有 10 个int类型的元素，而第二个在 32 位机器上将会输出 4，也就是指针的长度。

/*
指针与数组
    数组是 C 自带的基本数据结构，彻底理解数组及其用法是开发高效应用程序的基础。

数组和指针表示法紧密关联，在合适的上下文中可以互换。

站在编译器的角度讲，变量名、数组名都是一种符号，它们都是有类型的，它们最终都要和数据绑定起来。

变量名用来指代一份数据，数组名用来指代一组数据（数据集合），它们都是有类型的，以便推断出所指代的数据的长度。

编译器在编译过程中会创建一张专门的表格用来保存变量名及其对应的数据类型、地址、作用域等信息。

sizeof 是一个操作符，不是函数，使用 sizeof 时可以从这张表格中查询到   符号的长度。

所以，这里对数组名使用sizeof可以查询到数组实际的长度。
pa 仅仅是一个指向 int 类型的指针，编译器根本不知道它指向的是一个整数，还是一堆整数。
虽然在这里它指向的是一个数组，但数组也只是一块连续的内存，没有开始和结束标志，也没有额外的信息来记录数组到底多长。
所以对 pa 使用 sizeof 只能求得的是指针变量本身的长度。
也就是说，编译器并没有把 pa 和数组关联起来，pa 仅仅是一个指针变量，不管它指向哪里，sizeof求得的永远是它本身所占用的字节数。

作者：编程指北
链接：https://juejin.cn/post/6892199770106888199
来源：稀土掘金
著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。
*/

/*
二维数组
    不要认为二维数组在内存中就是按行、列这样二维存储的，实际上，不管二维、三维数组... 都是编译器的语法糖。
    存储上和一维数组没有本质区别： 都是一维线性排列

假设数组定义是这样的: int array[n][m]
    访问: array[a][b]
        那么被访问元素地址的计算方式就是: array + (m * a + b)
        这个就是二维数组在内存中的本质，其实和一维数组是一样的，只是语法糖包装成一个二维的样子。
*/
int num;
int *pi = &num; 
printf("address of pi: %p\n", pi);
void* pv = pi;
pi = (int*) pv; 
printf("address of pi: %p\n", pi);
/*
神奇的 void 指针
    void func(); // 没有返回值
    int func1(void); //参数为空。
在这些情况下，void 表达的意思就是没有返回值或者参数为空。

但是对于 void 型指针却表示通用指针，可以用来存放任何数据类型的引用。
    void *ptr;

void 指针最大的用处就是在 C 语言中实现泛型编程，因为任何指针都可以被赋给 void 指针，void 指针也可以被转换回原来的指针类型
并且这个过程指针实际所指向的地址并不会发生变化。

不能对 void 指针解引用
    int num;
    void *pv = (void*)&num;
    *pv = 4; // 错误

解引用的本质就是编译器根据指针所指的类型，然后从指针所指向的内存连续取 N 个字节，然后将这 N 个字节按照指针的类型去解释。
    比如 int *型指针，那么这里 N 就是 4，然后按照 int 的编码方式去解释数字
    但是 void，编译器是不知道它到底指向的是 int、double、或者是一个结构体，所以编译器没法对 void 型指针解引用。
*/